package com.xu.cloud.common.filter;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.xu.cloud.common.constant.Constant;
import com.xu.cloud.common.constant.OrderConstant;
import com.xu.cloud.common.enums.RequestBlockingEnum;
import com.xu.cloud.common.exception.ErrorCodeEnum;
import com.xu.cloud.common.pojo.Result;
import com.xu.cloud.common.pojo.domain.RequestBlockingInfoDO;
import com.xu.cloud.common.pojo.mapper.RequestBlockingInfoMapper;
import com.xu.cloud.common.sequence.ISequence;
import com.xu.cloud.common.sequence.UUIDSequence;
import com.xu.cloud.common.api.ApiContext;
import com.xu.cloud.common.util.IpUtil;
import com.xu.cloud.common.util.JsonUtil;
import com.xu.cloud.common.util.LocalDateTimeUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 交易参数设置过滤器
 *
 * @author xuguan
 * @since 2024/1/9
 */
@Order(value = OrderConstant.REQUEST_BLOCKING_INFO_FILTER_ORDER)
@WebFilter(description = "Http请求阻止过滤器", urlPatterns = "/*")
public class HttpRequestBlockingFilter extends OncePerRequestFilter {
    private static final Logger log = LoggerFactory.getLogger(HttpRequestBlockingFilter.class);
    private static final String BLOCKING_TYPE = RequestBlockingEnum.IP.getBlockingType();
    private static final String REDIS_PREFIX_REQUEST_BLOCKING = "REQUEST_BLOCKING:";

    // 阻止3s内超过3次的请求ip
    private static final Duration REQUEST_BLOCKING_DURATION = Duration.ofSeconds(3L);
    private static final int REQUEST_BLOCKING_COUNT = 3;
    @Autowired
    private ObjectProvider<ISequence> sequenceGeneratorObjectProvider;
    @Autowired(required = false)
    private StringRedisTemplate stringRedisTemplate;
    @Autowired(required = false)
    private RequestBlockingInfoMapper requestBlockingInfoMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 项目未配置缓存和数据库
        if (Objects.isNull(stringRedisTemplate) || Objects.isNull(requestBlockingInfoMapper)) {
            filterChain.doFilter(request, response);
            return;
        }

        final String clientIp = ApiContext.current().getClientIp();

        if (StringUtils.isEmpty(clientIp)) {
            blockingResponse(response);
            return;
        }

        // 内网ip放行
        if (IpUtil.isInternalIp(clientIp)) {
            filterChain.doFilter(request, response);
            return;
        }

        // 检查是否中国ip
        if (!IpUtil.isChinaIp(clientIp)) {
            saveBlockingInfo(BLOCKING_TYPE, clientIp);
            blockingResponse(response);
            return;
        }

        // 检查数据库是否有阻止信息
        if (!checkDbBlocking(BLOCKING_TYPE, clientIp)) {
            blockingResponse(response);
            return;
        }

        // 检查请求次数是否需要阻止
        if (!checkRedisBlocking(BLOCKING_TYPE, clientIp, REQUEST_BLOCKING_COUNT, REQUEST_BLOCKING_DURATION)) {
            blockingResponse(response);
            return;
        }

        // 添加请求次数缓存
        addRateLimitKey(BLOCKING_TYPE, clientIp, REQUEST_BLOCKING_DURATION);

        filterChain.doFilter(request, response);
    }

    private boolean checkDbBlocking(String blockingType, String blockingValue) {
        final RequestBlockingInfoDO requestBlockingInfoDo = getBlockingInfoDo(blockingType, blockingValue);

        if (Objects.isNull(requestBlockingInfoDo)) {
            return true;
        }

        // 已删除
        if (Objects.nonNull(requestBlockingInfoDo.getDeleted()) && Objects.equals(requestBlockingInfoDo.getDeleted(), 1)) {
            return true;
        }

        // 请求阻止时间到期
        final LocalDateTime currentDateTime = LocalDateTimeUtil.currentDateTime();
        if (Objects.nonNull(requestBlockingInfoDo.getBlockingTime()) && requestBlockingInfoDo.getBlockingTime().isBefore(currentDateTime)) {
            return true;
        }

        // 更新请求阻止次数
        updateBlockingCountById(requestBlockingInfoDo.getId(), requestBlockingInfoDo.getBlockingCount() + 1);

        return false;
    }

    private boolean checkRedisBlocking(String blockingType, String blockingValue, int limitCount, Duration limitDuration) {
        Assert.isTrue(limitCount > 0, "rate limit count must greater than zero");
        Assert.notNull(limitDuration, "limitDuration must not be null");

        final String rateLimitKey = REDIS_PREFIX_REQUEST_BLOCKING + blockingType + ":" + blockingValue;

        log.info("限流配置生效, 缓存key={}, 限流次数={}, 限流时间段={}",
                rateLimitKey, limitCount, limitDuration);

        final long reqMilli = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
        final long durationMillis = limitDuration.toMillis();

        final ZSetOperations<String, String> zSetOperations = stringRedisTemplate.opsForZSet();

        // 清空限流时间段之前的值
        zSetOperations.removeRangeByScore(rateLimitKey, 0, reqMilli - durationMillis);

        // 获取缓存中请求次数
        final Long count = zSetOperations.count(rateLimitKey, 0, reqMilli);
        Assert.notNull(count, "redis zSet count must not be null");
        log.info("限流时间段内请求次数为count={}", count);

        if (count >= limitCount) {
            saveBlockingInfo(blockingType, blockingValue);
            stringRedisTemplate.delete(rateLimitKey);
            return false;
        }

        return true;
    }

    private void addRateLimitKey(String blockingType, String blockingValue, Duration limitDuration) {
        final String rateLimitKey = REDIS_PREFIX_REQUEST_BLOCKING + blockingType + ":" + blockingValue;
        final ZSetOperations<String, String> zSetOperations = stringRedisTemplate.opsForZSet();
        final long reqMilli = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();

        zSetOperations.add(rateLimitKey, String.valueOf(reqMilli), reqMilli);
        stringRedisTemplate.expire(rateLimitKey, limitDuration.toMillis(), TimeUnit.MILLISECONDS);
    }

    private void saveBlockingInfo(String blockingType, String blockingValue) {
        final LocalDateTime currentDateTime = LocalDateTimeUtil.currentDateTime();
        final RequestBlockingInfoDO requestBlockingInfoDo = getBlockingInfoDo(blockingType, blockingValue);
        if (Objects.nonNull(requestBlockingInfoDo)) {
            long blockingCount = Objects.nonNull(requestBlockingInfoDo.getBlockingCount()) ? requestBlockingInfoDo.getBlockingCount() + 1 : 1;
            updateBlockingCountById(requestBlockingInfoDo.getId(), blockingCount);
        } else {
            final ISequence sequence = sequenceGeneratorObjectProvider.getIfAvailable(UUIDSequence::new);
            final RequestBlockingInfoDO insertRequestBlockingInfo = new RequestBlockingInfoDO();
            insertRequestBlockingInfo.setId(Long.parseLong(sequence.nextId()));
            insertRequestBlockingInfo.setBlockingType(blockingType);
            insertRequestBlockingInfo.setBlockingValue(blockingValue);
            insertRequestBlockingInfo.setBlockingCount(1L);
            insertRequestBlockingInfo.setDeleted(Constant.NOT_DELETED);
            insertRequestBlockingInfo.setCreateUser("admin");
            insertRequestBlockingInfo.setCreateTime(currentDateTime);
            requestBlockingInfoMapper.insert(insertRequestBlockingInfo);
        }
    }

    private void updateBlockingCountById(Long id, Long blockingCount) {
        final RequestBlockingInfoDO updateRequestBlockingInfo = new RequestBlockingInfoDO();
        updateRequestBlockingInfo.setId(id);
        updateRequestBlockingInfo.setBlockingCount(blockingCount);
        updateRequestBlockingInfo.setDeleted(Constant.NOT_DELETED);
        updateRequestBlockingInfo.setUpdateUser("admin");
        updateRequestBlockingInfo.setUpdateTime(LocalDateTimeUtil.currentDateTime());
        requestBlockingInfoMapper.updateById(updateRequestBlockingInfo);
    }

    private RequestBlockingInfoDO getBlockingInfoDo(String blockingType, String blockingValue) {
        final RequestBlockingInfoDO requestBlockingInfoDo = requestBlockingInfoMapper.selectOne(Wrappers.lambdaQuery(RequestBlockingInfoDO.class)
                .eq(RequestBlockingInfoDO::getBlockingType, blockingType)
                .eq(RequestBlockingInfoDO::getBlockingValue, blockingValue));
        return requestBlockingInfoDo;
    }

    private void blockingResponse(HttpServletResponse response) throws IOException {
        final ApiContext ctx = ApiContext.current();
        ctx.setCode(ErrorCodeEnum.ERR_REQUEST_ILLEGAL.getCode());
        ctx.setMsg(ErrorCodeEnum.ERR_REQUEST_ILLEGAL.getMessage());
        response.setStatus(Integer.parseInt(ErrorCodeEnum.ERR_REQUEST_ILLEGAL.getCode()));
        response.getWriter().println(JsonUtil.toJsonString(
                Result.fail(ErrorCodeEnum.ERR_REQUEST_ILLEGAL.getCode(), ErrorCodeEnum.ERR_REQUEST_ILLEGAL.getMessage(), null)));
        response.getWriter().flush();
    }
}
